/**
Copyright (c) 2008 New Gonzo <john@newgonzo.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
**/


package com.newgonzo.cannonball.css
{
	import flash.text.StyleSheet;
	import flash.utils.Dictionary;
	
	import com.newgonzo.commons.utils.XMLUtil;
	import com.newgonzo.commons.utils.URIUtil;
	
	import com.newgonzo.cannonball.css.CSSQuery;
	import com.newgonzo.cannonball.css.parser.CSSParser;
	
	import com.newgonzo.cannonball.w3c.css.sac.IDocumentHandler;
	import com.newgonzo.cannonball.w3c.css.sac.ILexicalUnit;
	import com.newgonzo.cannonball.w3c.css.sac.ISelectorList;
	import com.newgonzo.cannonball.w3c.css.sac.LexicalUnitTypes;
	import com.newgonzo.cannonball.w3c.dom.css.*;
	import com.newgonzo.cannonball.w3c.dom.stylesheets.IMediaList;
	
	import com.newgonzo.cannonball.css.parser.IExtendedSelector;
	
	
	public class CSSDocument
	{
		public static const ORIGIN_AUTHOR:String = "originAuthor";
		public static const ORIGIN_USER:String = "originUser";
		public static const ORIGIN_USER_AGENT:String = "originUserAgent";
		
		public static const DEFAULT_MEDIA:String = "screen";
		public static const PRIORITY_IMPORTANT:String = "important";
		
		private var docOrigin:String = ORIGIN_USER_AGENT; 	
		
		private var documentCascade:Array;
		private var sheets:Array;
		
		private var docUri:String;
		
		public function CSSDocument()
		{
			documentCascade = new Array();
			addDocument(this);
			sheets = new Array();
		}
		
		public function get origin():String
		{
			return docOrigin;
		}
		
		public function set origin(value:String):void
		{
			docOrigin = value;
		}
		
		public function get documentURI():String
		{
			return docUri;
		}
		
		public function set documentURI(value:String):void
		{
			docUri = value;
		}
		
		public function resolve(uri:String):String
		{
			return URIUtil.getAbsolute(uri, docUri);
		}
		
		public function addDocument(document:CSSDocument):CSSDocument
		{
			documentCascade.push(document);
			documentCascade.sort(documentSortFunc);
			return document;
		}
		
		public function removeDocument(document:CSSDocument):CSSDocument
		{
			var idx:int = documentCascade.indexOf(document);
			
			if(idx != -1)
			{
				return documentCascade.splice(idx, 1)[0];
				documentCascade.sort(documentSortFunc);
			}
			
			return null;
		}
		
		public function addStyleSheet(sheet:ICSSStyleSheet):ICSSStyleSheet
		{
			if(!sheet)
			{
				return null;
			}
			
			sheets.push(sheet);
			return sheet;
		}
		
		public function get styleSheets():Array
		{
			return sheets;
		}

		public function removeStyleSheet(sheet:ICSSStyleSheet):ICSSStyleSheet
		{
			var idx:int = sheets.indexOf(sheet);
			
			if(idx != -1)
			{
				return sheets.splice(idx, 1)[0] as ICSSStyleSheet;
			}
			
			return null;
		}
		
		public function parseCSS(css:String, uri:String = ""):ICSSStyleSheet
		{
			return addStyleSheet(parseStyleSheet(css, DEFAULT_MEDIA));
		}
		
		public function getMatchingRules(node:XML):ICSSRuleList
		{
			var document:CSSDocument;
			var sheet:ICSSStyleSheet;
			
			var rules:ICSSRuleList;
			var rule:ICSSRule;
			var styleRule:CSSStyleRule;
			
			var selectors:ISelectorList;
			var selector:IExtendedSelector;
			
			var matchingRules:CSSRuleList = new CSSRuleList();
			
			for each(document in documentCascade)
			{
				for each(sheet in document.styleSheets)
				{
					rules = sheet.cssRules;
					
					for each(rule in rules)
					{
						if(rule.type != CSSRuleTypes.STYLE_RULE)
						{
							continue;
						}
						
						styleRule = rule as CSSStyleRule;
						selectors = styleRule.selectors;
						
						for each(selector in selectors)
						{
							if(selector.match(node))
							{
								matchingRules.push(rule);
								
								// only need to add the rule once for a matching selector set
								break;
							}
						}
					}
				}
			}
			
			return matchingRules;
		}
		
		public function getCascadedStyle(node:XML):ICSSStyleDeclaration
		{
			var matchingRules:ICSSRuleList = getMatchingRules(node);
			
			// sort rules, placing highest specificities at the top
			sortRules(matchingRules);
			
			var styleRule:ICSSStyleRule;
			var styleDeclaration:ICSSStyleDeclaration = createStyleDeclaration(createValueProvider());
			
			// loop over rules backwards, getting to highest specificity last
			var count:uint = matchingRules.length;
			
			while(count--)
			{
				styleRule = matchingRules[count] as ICSSStyleRule;
				styleDeclaration = mergeStyles(styleDeclaration, styleRule.style);
			}
			
			return styleDeclaration;
		}
		
		public function parseStyleSheet(css:String, uri:String = "", media:String = ""):ICSSStyleSheet
		{			
			var parser:CSSParser = createParser();
			var handler:CSSDocumentHandler = createDocumentHandler();
			var sheet:ICSSStyleSheet = createStyleSheet(media);
			
			handler.currentDocument = this;
			handler.currentStyleSheet = sheet;
			parser.documentHandler = handler;
			parser.parse(css);
			
			return sheet;
		}
		
		public function parseStyleDeclaration(css:String):ICSSStyleDeclaration
		{
			var parser:CSSParser = createParser();
			var handler:CSSDocumentHandler = createDocumentHandler();
			var provider:CSSValueProvider = createValueProvider();
			
			handler.currentDocument = this;
			
			handler.currentValueProvider = provider;
			parser.documentHandler = handler;
			parser.parse(css);
			
			return createStyleDeclaration(provider);
		}
		
		public function mergeStyles(styles:ICSSStyleDeclaration, overrideStyles:ICSSStyleDeclaration):ICSSStyleDeclaration
		{
			var newProvider:CSSValueProvider = createValueProvider();
			
			var numProps:uint;
			var propName:String;
			var important:Boolean;
			
			numProps = styles.length;
			
			while(numProps--)
			{
				propName = styles.item(numProps);
				newProvider.setProperty(propName, styles.getPropertyCSSValue(propName), styles.getPropertyPriority(propName));
			}
			
			numProps = overrideStyles.length;
			
			while(numProps--)
			{
				propName = overrideStyles.item(numProps);
				
				// only override if the style isn't important.
				// if the override is important too, override
				important = overrideStyles.getPropertyPriority(propName) == PRIORITY_IMPORTANT;
				
				
				if(newProvider.getPropertyPriority(propName) == PRIORITY_IMPORTANT && !important)
				{
					continue;
				}
				
				// override property
				newProvider.setProperty(propName, overrideStyles.getPropertyCSSValue(propName), overrideStyles.getPropertyPriority(propName));
			}
			
			return createStyleDeclaration(newProvider);
		}
		
		public function getRuleSpecificity(rule:CSSStyleRule):uint
		{
			var selectors:ISelectorList = rule.selectors;
			var selector:IExtendedSelector;
			var highest:uint = 0;
			
			for each(selector in selectors)
			{
				if(selector.specificity > highest)
				{
					highest = selector.specificity;
				}
			}
			
			return highest;
		}
		
		public function createStyleSheet(media:String = ""):ICSSStyleSheet
		{
			return new CSSStyleSheet(media);
		}
		
		public function createStyleRule(selectors:ISelectorList, style:ICSSStyleDeclaration):ICSSStyleRule
		{
			return new CSSStyleRule(selectors, style);
		}
		
		public function createStyleDeclaration(provider:CSSValueProvider, parentRule:ICSSRule = null):ICSSStyleDeclaration
		{
			return new CSSStyleDeclaration(provider, parentRule);
		}
		
		public function createValueProvider():CSSValueProvider
		{
			return new CSSValueProvider();
		}
		
		public function createParser():CSSParser
		{
			return new CSSParser();
		}
		
		public function createDocumentHandler():CSSDocumentHandler
		{
			return new CSSDocumentHandler();	
		}
		
		public function createValue(unit:ILexicalUnit):ICSSValue
		{
			var value:ICSSValue;
			
			switch(unit.type)
			{
				case LexicalUnitTypes.SAC_URI:
					value = new CSSPrimitiveValue(unit.stringValue, CSSPrimitiveValueTypes.CSS_URI);
					break;
				
				case LexicalUnitTypes.SAC_IDENT:
					value = new CSSPrimitiveValue(unit.stringValue, CSSPrimitiveValueTypes.CSS_IDENT);
					break;
					
				case LexicalUnitTypes.SAC_STRING_VALUE:
					value = new CSSPrimitiveValue(unit.stringValue, CSSPrimitiveValueTypes.CSS_STRING);
					break;
					
				default:
					value = new CSSPrimitiveValue(unit.stringValue, CSSPrimitiveValueTypes.CSS_STRING);
					break;
			}
			
			return value;
		}
		
		private function sortRules(rules:ICSSRuleList):void
		{
			(rules as CSSRuleList).sort(ruleSortFunc);
		}
		
		private function ruleSortFunc(rule1:CSSStyleRule, rule2:CSSStyleRule):int
		{
			var spec1:uint = getRuleSpecificity(rule1);
			var spec2:uint = getRuleSpecificity(rule2);
			
			if(spec1 > spec2)
			{
				return -1;
			}
			
			if(spec1 < spec2)
			{
				return 1;
			}
			
			return 0;
		}
		
		private function documentSortFunc(doc1:CSSDocument, doc2:CSSDocument):int
		{
			if(doc1.origin == doc2.origin)
			{
				return 0;
			}
			
			if(doc1.origin == ORIGIN_AUTHOR)
			{
				return -1;
			}
			
			if(doc2.origin == ORIGIN_AUTHOR)
			{
				return 1;
			}
			
			if(doc1.origin == ORIGIN_USER)
			{
				return -1;
			}
			
			if(doc2.origin == ORIGIN_USER)
			{
				return 1;
			}
			
			if(doc1.origin == ORIGIN_USER_AGENT)
			{
				return -1;
			}
			
			return 1;
		}
		
		public function toString():String
		{
			return "[CSSDocument(documentURI=" + docUri + ")]";
		}
	}
}
